﻿/* ----------------------------------------------------------------------------
 * Copyright (c) Philip Stears 
 *
 * This source code is subject to terms and conditions of the Microsoft Public 
 * License (Ms-PL).
 * 
 * By using this source code in any fashion, you are agreeing to be bound 
 * by the terms of the Microsoft Public License.
 * 
 * To request a copy of the Microsoft Public License, send a request by email
 * to license@connectbasic.net.
 *
 * This notice must not be removed from this software.
 * ---------------------------------------------------------------------------- */

namespace ConnectBasic.Execution
{
    using System;
    using System.Reflection;
    using ConnectBasic.Language;
    using Microsoft.Linq.Expressions;
    using Microsoft.Scripting;
    using Microsoft.Scripting.Utils;

    internal class BasicCallBinder : InvokeBinder
    {
        public BasicCallBinder(CallInfo callInfo)
            : base(callInfo)
        {
        }

        public override DynamicMetaObject FallbackInvoke(DynamicMetaObject target, DynamicMetaObject[] args, DynamicMetaObject onBindingError)
        {

            // Get the executing code context (the first argument)
            var context = (BasicModule)args[0].Value;
            args = ArrayUtils.RemoveFirst(args);

            if (target.Value is string)
            {
                string targetName = (string)target.Value;                

                // Lookup a built-in function with the specified name
                foreach (var mi in typeof(BasicBuiltins).GetMethods(BindingFlags.Static | BindingFlags.Public))
                {

                    // Correct name?
                    if (!mi.Name.Equals(targetName))
                    {
                        continue;
                    }

                    // See if we can construct an MO for it
                    var result = GetRestrictionsAndArguments(target, args, mi, null);

                    if (result != null)
                    {
                        return new DynamicMetaObject(Expression.Call(mi, result.Item001), result.Item000);
                    }
                }

                // Not found, see if it's present in the scope
                BasicFunction lookupResult = context.GetFunction(targetName);

                if (lookupResult != null)
                {
                    var lookupResultDel = lookupResult.GetCallableDelegate();
                    var result = GetRestrictionsAndArguments(target, args, lookupResultDel.Method, lookupResultDel.Target);

                    if (result != null)
                    {

                        // Construct the call
                        return new DynamicMetaObject(
                                Expression.Call(Expression.Constant(lookupResultDel), lookupResultDel.GetType().GetMethod("Invoke"), result.Item001),
                                result.Item000);
                    }
                }
            }

            // Return an exception            
            return onBindingError ?? DynamicMetaObject.CreateThrow(target, args, typeof(NotImplementedException), (object[])null);
        }

        private static Tuple<BindingRestrictions, Expression[]> GetRestrictionsAndArguments(DynamicMetaObject target, DynamicMetaObject[] args, MethodInfo mi, object instance)
        {

            // Correct param count?
            var miParams = mi.GetParameters();

            if (instance != null)
            {
                miParams = ArrayUtils.RemoveFirst(miParams);
            }

            if (miParams.Length != args.Length)
            {
                return null;
            }

            // Correct args?
            Expression[] passedArgs = new Expression[args.Length];
            BindingRestrictions restrictions = BindingRestrictions.GetExpressionRestriction(
                Expression.Call(
                    typeof(string).GetMethod("Equals", BindingFlags.Static | BindingFlags.Public, null, new Type[] { typeof(string), typeof(string), typeof(StringComparison) }, null),
                    Expression.Constant(target.Value),
                    target.Expression,
                    Expression.Constant(StringComparison.OrdinalIgnoreCase)
                    ));

            for (int i = 0; i < miParams.Length; i++)
            {
                var param = miParams[i];
                var argument = args[i];

                if (argument.Value == null)
                {
                    if (!param.ParameterType.IsValueType)
                    {
                        passedArgs[i] = Expression.Constant(null, param.ParameterType);
                        restrictions = restrictions.Merge(
                            BindingRestrictions.GetExpressionRestriction(Expression.Equal(Expression.Constant(null), argument.Expression))
                            );
                    }
                    else
                    {

                        // Can't pass null into a value-type argument (TODO: Are should we?)
                        return null;
                    }
                }
                else
                {
                    if (param.ParameterType.IsAssignableFrom(argument.RuntimeType))
                    {

                        // Note: we need the convert operation to appease the call method because the expression
                        // type may not match the runtime type
                        passedArgs[i] = Expression.Convert(argument.Expression, param.ParameterType);
                        restrictions = restrictions.Merge(BindingRestrictions.GetTypeRestriction(args[i].Expression, miParams[i].ParameterType));
                    }
                    else
                    {
                        // TODO: Conversion
                        return null;
                    }
                }
            }

            // Return the tuple
            return new Tuple<BindingRestrictions, Expression[]>(restrictions, passedArgs);
        }
    }
}
